﻿using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OverStore.Configuration;
using OverStore.Configuration.Builder.Fluent;
using OverStore.Exceptions;
using OverStore.Runtime.OverStoreContext;
using OverStore.Runtime.Storage;
using OverStore.Scoping;
using OverStore.Configuration.Storage;
using OverStore.Runtime.Logger;
using OverStore.Configuration.Components;
using OverStore.Configuration.Builder;
using System.Reflection;
using OverStore.Scoping.Manager;
using OverStore.Runtime.ObjectRepository.CommandSet;
using OverStore.Runtime.ObjectRepository.Mapping;

namespace OverStore.TutorialAsTest.Tutorial_2._Work_With_PersistenceSession._Part_2._Saving
{
    /// <summary>
    /// Test correctness of all transitions described in table in Lesson 5.
    /// </summary>
    [TestClass]
    public class StateTransitionTest
    {
        class Product
        {
            public int Id { get; set; }

            public string Name { get; set; }
        }

        class DataStorageMock : IDataStorage, IComponentConfiguration<IDataStorage>
        {
            private OperationScopeManager _ScopeManager = new OperationScopeManager();

            public TypedRecord Execute(ICommand command, IRecord parameters, out int rowsAffected)
            {
                var c = (CommandMock)command;
                c.ExecuteCounter += 1;
                rowsAffected = 1;
                var id = parameters.GetValue<int>("Id");
                if (id != 2)
                {
                    rowsAffected = 0;
                    return null;
                }
                return new DictionaryTypedRecord(new KeyValuePair<string, object>("Id", 2),
                    new KeyValuePair<string, object>("Name", "OverStore"));
            }

            public IEnumerable<TypedRecord> ReadAll(ICommand command, IRecord parameters)
            {
                var c = (CommandMock)command;
                c.ReadAllCounter += 1;
                if (parameters.GetValue<int>("Id") != 2)
                    yield break;
                yield return new DictionaryTypedRecord(new KeyValuePair<string, object>("Id", 2),
                    new KeyValuePair<string, object>("Name", "OverStore"));
            }



            public IDataStorage CreateComponent(IOverStoreContext context)
            {
                return new DataStorageMock();
            }

            public bool Disposed { get { return false; } }

            public OperationScope OpenScope()
            {
                var r = new OperationScope();
                _ScopeManager.StartOperationScope(r);
                return r;
            }

            #region IDataStorage Members

            public OverStore.Sql.SqlFormatting SqlFormatting
            {
                get { throw new NotImplementedException(); }
            }

            #endregion
        }

        /// <summary>
        /// Command that can select data for single 
        /// Product { Id = 2, Name = "OverStore" }
        /// </summary>
        class CommandMock : ICommand
        {
            public int ReadAllCounter { get; set; }
            public int ExecuteCounter { get; set; }
        }

        class CommandSetMock : ICommandSet
        {
            public CommandSetMock()
            {
                SelectCommand = new CommandMock();
                InsertCommand = new CommandMock();
                UpdateCommand = new CommandMock();
                DeleteCommand = new CommandMock();
            }

            public CommandMock SelectCommand { get; private set; }

            ICommand ICommandSet.SelectCommand { get { return SelectCommand; } }

            public CommandMock InsertCommand { get; private set; }

            ICommand ICommandSet.InsertCommand { get { return InsertCommand; } }

            public CommandMock UpdateCommand { get; private set; }

            ICommand ICommandSet.UpdateCommand { get { return UpdateCommand; } }

            public CommandMock DeleteCommand { get; private set; }

            ICommand ICommandSet.DeleteCommand { get { return DeleteCommand; } }

            private IEnumerable<CommandMock> AllCommands
            {
                get
                {
                    yield return SelectCommand;
                    yield return InsertCommand;
                    yield return UpdateCommand;
                    yield return DeleteCommand;
                }
            }

            public int TotalExec
            {
                get { return AllCommands.Select(c => c.ExecuteCounter).Sum(); }
            }

            public int TotalReadAll
            {
                get { return AllCommands.Select(c => c.ReadAllCounter).Sum(); }
            }
        }

        class CommandSetMockConfiguration : IComponentConfiguration<ICommandSet>
        {
            private readonly CommandSetMock _Instance = new CommandSetMock();

            public CommandSetMock Instance
            {
                get { return _Instance; }
            }

            public ICommandSet CreateComponent(IOverStoreContext currentContext)
            {
                return Instance;
            }
        }

        static IComponentConfiguration<IMapper<Product>> CreateProductMapper()
        {
            return Component.Mapper.Delegate<Product>(
                (item, name, p) =>
                {
                    if (name == "Id")
                        return item.Id;
                    if (name == "Name")
                        return item.Name;
                    if (name == "orig__oid")
                        return p.GetIdentifier<Product, int>(item);
                    return Missing.Value;
                },
                (data, item, p) =>
                {
                    item.Name = data.GetString("Name");
                });
        }

        static OverStoreConfiguration CreateProductConfig(CommandSetMockConfiguration commandSet)
        {
            return OverStoreFluent.Configure()
                .LogToConsole()
                .WithDataStorage(new DataStorageMock())
                .CustomClassRepository<Product, int>()
                    .WithDepot(Component.Depot.WithInternalKey<Product, int>("Id"))
                    .WithMapper(CreateProductMapper())
                    .WithCommands(commandSet)
                    .WithChangesTracker(Component.InstanceChangesTracker.Checksum<Product, int>("Id", "Name"))
                    .ByIdentifierAttaching(i => i.Id)
                .EndObjectRepository()
            .End();
        }

        #region C_U

        [TestMethod]
        public void C_U_Reload()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.Reload(p);
            Assert.AreEqual(2, cs.Instance.SelectCommand.ExecuteCounter);
            Assert.AreEqual(2, cs.Instance.TotalExec);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
        }

        [TestMethod]
        public void C_U_Save()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.Save(p);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_U_MarkAsDeleted()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.MarkAsDeleted(p);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(1, cs.Instance.TotalExec);

            session.Save(p);
            Assert.AreEqual(1, cs.Instance.DeleteCommand.ExecuteCounter); // Object really deleted.
        }

        [TestMethod]
        public void C_U_Add()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.Add(p);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(1, cs.Instance.TotalExec);
            session.Save(p);
            // object really unchanged, e.w. is not saved.
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_U_Get()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            var p2 = session.GetByIdentifier<Product, int>(2);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(1, cs.Instance.TotalExec);
            session.Save(p);
            // object really unchanged, e.w. is not saved.
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_U_MakeChanges()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            p.Name = "oovovov"; // act
            session.Save(p);
            // object really modified
            Assert.AreEqual(1, cs.Instance.UpdateCommand.ExecuteCounter);
        }

        #endregion

        #region C_A

        [TestMethod]
        public void C_A_Reload_Err()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 4, Name = "OS" };
            // Product with Id = 4 does not exists in our mock,
            // so we should get an error.
            Assert.IsFalse(session.Reload(p));
        }

        [TestMethod]
        public void C_A_Reload_KnownIdentifier()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            // Product with Id = 2 exists in our mock,
            // so we should be reloaded without errors.
            session.Reload(p);
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
        }

        [TestMethod]
        public void C_A_Save()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
            Assert.AreEqual(1, cs.Instance.TotalExec);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
        }

        [TestMethod]
        [ExpectedException(typeof(PersistenceOperationFailedException))]
        public void C_A_MarkAsDeleted()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            session.MarkAsDeleted(p);
        }

        [TestMethod]
        public void C_A_Add()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            session.Add(p);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(0, cs.Instance.TotalExec);
            session.Save(p);
            // object really unchanged, e.w. is not saved.
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
        }

        [TestMethod]
        public void C_A_Get()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            session.Add(p);
            var p2 = session.GetByIdentifier<Product, int>(2);
            Assert.AreSame(p, p2);
            Assert.AreEqual(0, cs.Instance.TotalExec);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            session.Save(p);
            // object really added, e.w. is saved.
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
        }

        [TestMethod]
        public void C_A_MakeChanges()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "OS" };
            p.Name = "oovovov"; // act
            session.Save(p);
            // object really modified
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        #endregion

        #region C_M

        [TestMethod]
        public void C_M_Reload()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            string name = p.Name;
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            p.Name = "osososs";
            session.Reload(p);
            Assert.AreEqual(2, cs.Instance.SelectCommand.ExecuteCounter);
            Assert.AreEqual(2, cs.Instance.TotalExec);
            Assert.AreEqual(0, cs.Instance.TotalReadAll);
            Assert.AreEqual(name, p.Name);
        }

        [TestMethod]
        public void C_M_Save()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            p.Name = "osososos";
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.UpdateCommand.ExecuteCounter); // Object really updated
            Assert.AreEqual(2, cs.Instance.TotalExec); // Read + Update 
            session.Save(p); // Object already saved, so no command execution needed
            Assert.AreEqual(2, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_M_MarkAsDeleted()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            p.Name = "osososos";
            session.MarkAsDeleted(p);
            Assert.AreEqual(1, cs.Instance.TotalExec); // Marking as deleted does not cause deleting from data storage.
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.DeleteCommand.ExecuteCounter); // Object really deleted.
        }

        [TestMethod]
        public void C_M_Add()
        {
            var cs = new CommandSetMockConfiguration();
            using (var session = CreateProductConfig(cs).CreateSession())
            using (var scope = session.OpenScope())
            {
                var p = session.GetByIdentifier<Product, int>(2); // Get captured object
                Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
                p.Name = "ososos";
                session.Add(p);
                Assert.AreEqual(1, cs.Instance.TotalExec);
                session.Save(p);
                // object is changed and should be saved.
                Assert.AreEqual(1, cs.Instance.UpdateCommand.ExecuteCounter);
                scope.Complete();
            }
        }

        [TestMethod]
        public void C_M_Get()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            p.Name = "sdsd";
            var p2 = session.GetByIdentifier<Product, int>(2);
            Assert.AreEqual(1, cs.Instance.TotalExec);
            session.Save(p);
            // object is changed and should be saved.
            Assert.AreEqual(1, cs.Instance.UpdateCommand.ExecuteCounter);
        }

        #endregion

        #region C_D

        [TestMethod]
        public void C_D_Reload()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            session.MarkAsDeleted(p);
            session.Reload(p);
            Assert.AreEqual(2, cs.Instance.SelectCommand.ExecuteCounter); // GetByIdentifier + Reload
            Assert.AreEqual(2, cs.Instance.TotalExec);
            session.Save(p);
            Assert.AreEqual(2, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_D_Save()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            session.MarkAsDeleted(p);
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.DeleteCommand.ExecuteCounter);
            Assert.AreEqual(2, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_D_MarkAsDeleted()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            session.MarkAsDeleted(p);
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.MarkAsDeleted(p);
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_D_Add_Unchanged()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            session.MarkAsDeleted(p);
            Assert.AreEqual(1, cs.Instance.TotalExec);
            session.Add(p);
            // object was unchanged, so addition should
            // cancel delete mark and does not cause updating.
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_D_Add_Modified()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            session.MarkAsDeleted(p);
            p.Name = "ososossos";
            session.Add(p);
            // object is changed, so addition should
            // cancel delete mark and cause updating.
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.UpdateCommand.ExecuteCounter);
            Assert.AreEqual(2, cs.Instance.TotalExec);
        }

        [TestMethod]
        public void C_D_Get()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2); // Get captured object
            session.MarkAsDeleted(p);
            var p1 = session.GetByIdentifier<Product, int>(2);
            Assert.AreSame(p, p1);
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
        }

        [TestMethod]
        public void C_D_MakeChange()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = session.GetByIdentifier<Product, int>(2);
            session.MarkAsDeleted(p);
            p.Name = "sssdsdasd";
            session.Save(p);
            Assert.AreEqual(0, cs.Instance.UpdateCommand.ExecuteCounter);
            Assert.AreEqual(1, cs.Instance.DeleteCommand.ExecuteCounter);
        }

        #endregion

        #region F_U

        [TestMethod]
        public void F_U_Reload_Ok()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "ssdsd" };
            session.Reload(p);
            Assert.AreEqual(1, cs.Instance.SelectCommand.ExecuteCounter);
            Assert.AreEqual("OverStore", p.Name);
        }

        [TestMethod]
        public void F_U_Reload_Error()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 3, Name = "ssdsd" };
            Assert.IsFalse(session.Reload(p));
        }

        [TestMethod]
        public void F_U_Save_Ok()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "ssdsd" };
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
            Assert.AreEqual(1, cs.Instance.TotalExec);
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void F_U_Save_ErrorOnInsertingDuplicateKeys()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "ssdsd" };
            session.Save(p);
            if (cs.Instance.InsertCommand.ExecuteCounter != 0)
                throw new InvalidOperationException(
                    "Simulate error on inserting duplicate primary key value");
        }

        [TestMethod]
        [ExpectedException(typeof(PersistenceOperationFailedException))]
        public void F_U_MarkAsDeleted_Ok()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "os" };
            session.MarkAsDeleted(p);
        }

        [TestMethod]
        [ExpectedException(typeof(IdentityReferenceIntegrityException))]
        public void F_U_MarkAsDeleted_IREEx()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "os" };
            var p1 = session.GetByIdentifier<Product, int>(2);
            session.MarkAsDeleted(p); // Try mark as delete object with key = 2
        }

        [TestMethod]
        public void F_U_Add_Ok()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "os" };
            session.Add(p);
            Assert.AreEqual(0, cs.Instance.TotalExec);
            session.Save(p);
            Assert.AreEqual(1, cs.Instance.InsertCommand.ExecuteCounter);
        }

        [TestMethod]
        [ExpectedException(typeof(IdentityReferenceIntegrityException))]
        public void F_U_Add_IREEx()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "os" };
            var p1 = session.GetByIdentifier<Product, int>(2);
            session.Add(p);
        }

        [TestMethod]
        public void F_U_Get()
        {
            var cs = new CommandSetMockConfiguration();
            var session = CreateProductConfig(cs).CreateSession();
            var p = new Product { Id = 2, Name = "os" };
            var p1 = session.GetByIdentifier<Product, int>(2);
            Assert.AreNotSame(p, p1);
        }

        #endregion
    }
}
